/*
 * BlackLightning.Dispatcher
 * Andy Tidball
 * 
 * Project: Black Lightning
 * Copyright: GNU General Public License
 */

using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using BlackLightning.PluginLib.Modules;
using BLLib.Net;

namespace BlackLightning {
	/// <summary>
	/// Receives packets from both ends, sends them through the module chain, and delivers them to their destination.
	/// </summary>
	internal class Dispatcher {
		// constants
		private const string FrontEndThreadName   = "Front End Receiving Thread";
		private const string GameServerThreadName = "Game Server Receiving Thread";
		private const string DispatchThreadName   = "Packet Dispatch Thread";
		private const string StopThreadName       = "Dispatcher Shutdown Thread";

		// member data
		private BlackLightning   _BlackLightning;   // the instance of BlackLightning that owns the Dispatcher
		private FrontEnd         _FrontEnd;         // the front end we're connected to
		private GameServer       _GameServer;       // the game server we're connected to
		private Thread           _FrontEndThread;   // the thread listening for data from the front end
		private Thread           _GameServerThread; // the thread listening for data from the game server
		private Thread           _DispatchThread;   // the thread dispatching packets down the module chain and delivering them
		private Thread           _StopThread;       // the thread that handles shutting the dispatcher down
		private ManualResetEvent _StopDispatcher;   // signalled when the dispatcher should be stopped and shutdown
		private ManualResetEvent _PacketsAvailable; // signalled when there are patckets in need of dispatching
		private Queue<BLPacket>  _PacketQueue;      // the queue of packets waiting to be dispatched
		private Mutex            _PacketQueueMutex; // a mutex controlling access to the packet queue
		private bool             _Stop;             // flag that controls when the other threads exit

		/// <summary>
		/// Creates a new Dispatcher.
		/// </summary>
		/// <param name="BL">The instance of BlackLightning that will own the Dispatcher.</param>
		/// <param name="FE">The front end to dispatch to/from.</param>
		/// <param name="GS">The game server to dispatch to/from.</param>
		public Dispatcher(BlackLightning BL, FrontEnd FE, GameServer GS) {
			// setup our member data
			_BlackLightning   = BL;
			_FrontEnd         = FE;
			_GameServer       = GS;
			_FrontEndThread   = new Thread(FrontEndMain);
			_GameServerThread = new Thread(GameServerMain);
			_DispatchThread   = new Thread(DispatchMain);
			_StopThread       = new Thread(StopMain);
			_StopDispatcher   = new ManualResetEvent(false);
			_PacketsAvailable = new ManualResetEvent(false);
			_PacketQueue      = new Queue<BLPacket>();
			_PacketQueueMutex = new Mutex();
			_Stop             = false;

			// name our threads for ease of use
			_FrontEndThread.Name   = FrontEndThreadName;
			_GameServerThread.Name = GameServerThreadName;
			_DispatchThread.Name   = DispatchThreadName;
			_StopThread.Name       = StopThreadName;

			// wire up events for the front end and game server
			_FrontEnd.PacketReceived   += PacketReceived;
			_FrontEnd.Disconnected     += FrontEndDisconnected;
			_GameServer.PacketReceived += PacketReceived;
			_GameServer.Disconnected   += GameServerDisconnected;
		}

		/// <summary>
		/// Gets the FrontEnd that the Dispatcher is connected to.
		/// </summary>
		public FrontEnd FrontEnd {
			get {
				return _FrontEnd;
			}
		}

		/// <summary>
		/// Gets the GameServer that the Dispatcher is connected to.
		/// </summary>
		public GameServer GameServer {
			get {
				return _GameServer;
			}
		}

		/// <summary>
		/// Starts the dispatcher listening for and dispatching packets.
		/// </summary>
		public void Run() {
			// start our threads
			_Stop = false;
			_StopThread.Start();
			_DispatchThread.Start();
			_FrontEndThread.Start();
			_GameServerThread.Start();
			Thread.Sleep(0); // give the other threads a chance to get scheduled
		}

		/// <summary>
		/// Disconnects from the current GameServer, then replaces it with the given GameServer.
		/// </summary>
		/// <param name="GS">The GameServer to replace the current one with.</param>
		public void Reconnect(GameServer GS) {
			// disconnect from the current game server
			_GameServer.Disconnect();

			// make sure that the game server thread is dead
			_GameServerThread.Join();

			// unwire its events
			_GameServer.PacketReceived -= PacketReceived;
			_GameServer.Disconnected   -= GameServerDisconnected;

			// replace the old game server with the new
			_GameServer = GS;

			// wire its events
			_GameServer.PacketReceived += PacketReceived;
			_GameServer.Disconnected   += GameServerDisconnected;

			// make a new game server thread and run it
			_GameServerThread = new Thread(GameServerMain);
			_GameServerThread.Name = GameServerThreadName;
			_GameServerThread.Start();
		}

		/// <summary>
		/// Stops the dispatcher from dispatching packets.  Make sure this is called before disconnecting from either end.
		/// </summary>
		public void Stop() {
			// signal the stop thread
			_StopDispatcher.Set();
		}

		/// <summary>
		/// Sends a packet down the module chain and out to its destination.
		/// </summary>
		/// <param name="P">The Packet to send.</param>
		public void SendPacket(BLPacket P) {
			// if we're in the process of stopping, then sending a packet won't work
			if (_Stop) {
				return;
			}

			PacketReceived(this, new PacketReceivedEventArgs(P));
		}

		/// <summary>
		/// The main method for the StopThread.
		/// </summary>
		private void StopMain() {
			// wait until someone signals us to shutdown
			_StopDispatcher.WaitOne();

			// set the stop flag
			_Stop = true;

			// disconnect from both ends
			_FrontEnd.Disconnect();
			_GameServer.Disconnect();

			// tell the dispatch thread that packets are available so it wakes up and realizes we're stopping
			_PacketsAvailable.Set();

			// wait for the other threads to exit
			_FrontEndThread.Join();
			_GameServerThread.Join();
			_DispatchThread.Join();

			// release our resources
			_StopDispatcher.Close();
			_PacketsAvailable.Close();
			_PacketQueueMutex.Close();

			// stop the program
			_BlackLightning.Stop();
		}

		/// <summary>
		/// The main method for the FrontEndThread.
		/// </summary>
		private void FrontEndMain() {
			// keep going until the stop flag is set or we get disconnected
			while (!_Stop && _FrontEnd.Connected) {
				// wait for some data to come in, this will call PacketReceived when a packet arrives
				_FrontEnd.CheckForPackets();
			}
		}

		/// <summary>
		/// Called if the FrontEnd becomes disconnected.
		/// </summary>
		private void FrontEndDisconnected(object Sender, DisconnectedEventArgs e) {
			// stop the dispatcher, we can't keep going without the front end
			Stop();
		}

		/// <summary>
		/// The main method for the GameServerThread.
		/// </summary>
		private void GameServerMain() {
			// keep going until the stop flag is set or we get disconnected
			while (!_Stop && _GameServer.Connected) {
				// wait for some data to come in, this will call PacketReceived when a packet arrives
				_GameServer.CheckForPackets();
			}
		}

		/// <summary>
		/// Called if the GameServer becomes disconnected.
		/// </summary>
		private void GameServerDisconnected(object Sender, DisconnectedEventArgs e) {
			// let the user know that we lost the connection
			if (_FrontEnd.Connected) {
				PacketReceived(null, new PacketReceivedEventArgs(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Black Lightning lost connection to game server.")));
			}
		}

		/// <summary>
		/// Called whenever a packet is received.
		/// </summary>
		/// <param name="Sender">The FrontEnd or GameServer that received a packet.</param>
		/// <param name="e">Arguments about the Packet received.</param>
		private void PacketReceived(object Sender, PacketReceivedEventArgs e) {
			// add the packet to the queue and signal the dispatch thread
			_PacketQueueMutex.WaitOne();
			_PacketQueue.Enqueue(e.Packet as BLPacket);
			_PacketsAvailable.Set();
			_PacketQueueMutex.ReleaseMutex();
		}

		/// <summary>
		/// The main method for the DispatchThread.
		/// </summary>
		private void DispatchMain() {
			// keep going until the stop flag is set
			while (!_Stop) {
				// wait until there are packets in the queue
				_PacketsAvailable.WaitOne();

				// if the flag has been set while we were waiting, ignore any packets and exit
				if (_Stop) {
					break;
				}

				// copy the queue and clear it
				_PacketQueueMutex.WaitOne();
				BLPacket[] Packets = new BLPacket[_PacketQueue.Count];
				_PacketQueue.CopyTo(Packets, 0);
				_PacketQueue.Clear();
				_PacketsAvailable.Reset();
				_PacketQueueMutex.ReleaseMutex();

				// run through each packet, send it down the module chain, and deliver it
				foreach (BLPacket P in Packets) {
					SendPacketToModules(P);
					DeliverPacket(P);
				}
			}
		}

		/// <summary>
		/// Determines which Modules are interested in a packet and then sends it to them in the proper order.
		/// </summary>
		/// <param name="P">The packet to send down the module chain</param>
		private void SendPacketToModules(BLPacket P) {
			// create a query for this packet
			RegistrationQuery Query = _BlackLightning.PacketRegistry.Query(P);
			bool SentToAModule = false;

			// run through each registration in the query and handle it
			PacketRegistration Registration = Query.GetNext(P);
			while (Registration != null) {
				// check if they want to receive the packet in a new thread or not
				if (Registration.NewThread) {
					// send the packet to the object that registered for it in a new thread
					ThreadPool.QueueUserWorkItem(SendPacketMain, new object[]{Registration, P.Clone()});
					SentToAModule = true;
				} else {
					// send the packet to the object that registered for it in this thread
					Registration.Object.HandlePacket(ref P);
					SentToAModule = true;

					// if the packet was suppressed, then stop handling it
					if (P.Suppressed) {
						break;
					}
				}

				// get the next state of the packet
				P = P.GetNextState();

				// get the next registration
				Registration = Query.GetNext(P);
			}

			// if the packet was destined for Black Lightning and we didn't send the packet to any modules, display an error
			if (P.Destination == Program.BlackLightning && !SentToAModule) {
				SendPacket(new RawBLPacket(Program.BlackLightning, Program.FrontEnd, "Command was not handled by any loaded plugins or modules."));
			}
		}

		/// <summary>
		/// Method to be executed in a new thread which sends a packet to an object.
		/// </summary>
		/// <param name="State">An object[] whose first element is a PacketRegistration and second is a BLPacket.</param>
		private void SendPacketMain(object State) {
			// cast the argument down to get to what we need
			object[]           StateArray   = State         as object[];
			PacketRegistration Registration = StateArray[0] as PacketRegistration;
			BLPacket           Packet       = StateArray[1] as BLPacket;

			// send the packet out
			Registration.Object.HandlePacket(ref Packet);
		}

		/// <summary>
		/// Delivers a packet to its destination.
		/// </summary>
		/// <param name="P">The packet to deliver.</param>
		private void DeliverPacket(BLPacket P) {
			if (P.Destination == Program.FrontEnd) {
				_FrontEnd.SendPacket(P);
			} else if (P.Destination == Program.GameServer) {
				_GameServer.SendPacket(P);
			}
		}
	}
}
